第10节 js常用进阶知识

问题

  1. 基本数据类型和引用数据类型有什么区别

  2. 垃圾回收机制

    1. 垃圾回收机制是什么
    2. gc策略是什么
    3. 如何减少gc开销
    4. 如何优化gc
    5. 内存泄漏原因有哪些
  3. 函数递归是什么

  4. 谈谈js异步编程?

    或问 js事件循环机制是什么?

    或问 什么是Event Loop?

js进阶注意事项

行业敲门砖

技术为王

web前端岗位竞争对手

  1. 应届毕业生(计算机)
  2. 自学者(转行)
  3. 社会上的web前端工程师
  4. 其他机构的学员

核心竞争能力

  1. js进阶
  2. 项目
  3. 就业辅导

js进阶怎么学

  1. 理解
  2. 背诵(用自己的话来总结) => 关系到待遇的高低

(一) 基本数据类型和引用数据类型的区别

变量在内存中的存储方式

复习:

  • 基本数据类型有哪些
    • 数字
    • 字符串
    • 布尔
    • null
    • undefined
    • 对象
    • symbol(es6)
  • 引用数据类型有哪些
    • 纯对像
    • 数组
    • 函数
    • .......

基本数据类型和引用数据类型数据在内存中的存储

  • 基本数据类型存放在 栈区

  • 引用数据类型存放在 堆区, 同时在栈区存放数据在堆区的地址(引用)

var num = 100;
var str = 'hello';

var obj = {
    age:100,
    username: '张三'
}

以上数据在内存中的存储如图所示

image-20220209100017732

练习: 说出下面代码运行的结果, 并说说为什么

var a = 100;
var b = a;
b = 200;
console.log(a); // 100 

var obj1 = {
   name: '张三',
   age: 10
}
var obj2 = obj1;
obj2.name = '李四';

console.log(obj1.name);   
console.log(obj2.name); 

标题

js垃圾回收机制和内存泄漏

(二) 函数的调用方式

  1. 普通调用
  2. 回调
  3. 递归调用
  4. 自调用
// 普通调用(略)

// 回调函数: 函数作为参数
// callback是个函数
function foo(callback) {
  callback(100); // callback就是goo(100)
} 
function goo(num) {
  console.log(num);
} 
foo(goo); 

// jquery中的回调函数 
$.ajax({
  type:'get',
  url: 'xxxxxx',
  dataType:'json',
  // 成功时的回调函数
  success: function(res) {
    console.log(res);
  },
  // 失败时的回调函数
  error: function(err) {
    console.log(err);
  }
})

// 递归是指函数自己调用自己
function say() {
  say();
}
// 不使用for循环进行数字累加 
var sum = 0;
function add(i) {
  sum = sum + i;
  console.log(sum);
  ++i;
  if (i <= 100) {
    add(i);
  }
}
add(1);

// 自调用:可以避免全局变量污染
(function(c){
  var a = 100;
  var b = 200;
  console.log(c);
})(300); 

思考: 如何使用递归函数, 用setTimeout模拟定时器

(三) js垃圾回收机制

垃圾回收机制是什么

垃圾回收机制(GC:Garbage Collection),执行环境负责管理代码执行过程中使用的内存。垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

垃圾回收策略是什么

2种最为常用:标记清除和引用计数,其中标记清除更为常用。

标记清除(mark-and-sweep):是对于脱离作用域的变量进行回收,当进入作用域时,进行标记,离开作用域时,标记并回收这些变量。到目前为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。 当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能用到它们。而当变量离开环境时,这将其 标记为“离开环境”。

引用计数:引用计数是跟踪记录每个值被引用的次数。就是变量的引用次数,被引用一次则加1,当这个引用计数为0时,被视为准备回收的对象,每当过一段时间开始垃圾回收的时候,就把被引用数为0的变量回收。引用计数方法可能导致循环引用,类似死锁,导致内存泄露。

如何减少垃圾回收开销

由于每次的垃圾回收开销都相对较大,并且由于机制的一些不完善的地方,可能会导致内存泄露,我们可以利用一些方法减少垃圾回收,并且尽量避免循环引用。

  1. 在对象结束使用后 ,令obj = null。这样利于解除循环引用,使得无用变量及时被回收;

  2. js中开辟空间的操作有new XX() 比如new Date() , [ ], { }, function (){..}。尽量减少此类操作, 最大限度的实现对象的重用;举例:

     var arr = [1,2,3,4];
     arr = []; // 清空数组-不好的做法
     arr.length = 0;  // 清空数组,好的做法
    
  3. 慎用闭包。闭包容易引起内存泄露。本来在函数返回之后,之前的空间都会被回收。但是由于闭包可能保存着函数内部变量的引用,且闭包在外部环境,就会导致函数内部的变量不能够销毁。

如何优化垃圾回收

分代回收(Generation GC):与Java回收策略思想是一致的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。 增量GC:这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推。

常见内存泄露的原因

  • 全局变量滥用引起的内存泄露
  • 闭包引起的内存泄露:慎用闭包
  • dom清空或删除时,事件未清除导致的内存泄漏
  • 循环引用带来的内存泄露

参考连接: https://blog.csdn.net/qq_21428081/article/details/82465801https://www.cnblogs.com/kaicy/p/14715750.html

总结:

  1. 什么是gc
  2. gc策略
  3. 如何减少垃圾回收开销
  4. gc优化
  5. 内存泄漏的原因

(四) js异步编程

(1) 同步和异步(理解)

js中的同步和异步跟生活中的同步异步意思有些不太一样

js的同步指的是,同一时间只执行一个任务, 当一个任务结束以后才能执行下一个任务。

js的异步指的是,同一时间可以执行多个任务,常见的js异步操作有setTimeout(), setInterval(), ajax请求等。

(2) 单线程和多线程(理解)

单线程: 同一时间只能做一件事 多线程: 同一时间可以做多件事 JavaScript语言的一大特点就是单线程, 那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。比如java和c#就是多线程的语言。 JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

(3) JS中的异步运行机制

也称js事件循环机制(Event Loop)
JS是单线程的,那么他是如何是实现异步操作的? JS中的异步运行机制(背诵):

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件(事件一般都有对应的回调函数)。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行任务(实际上就是执行任务对应的回调函数)。

(4)主线程不断重复上面的前三步。

举例说明:

<script>
    setTimeout(function(){
        console.log('时间到了');
    }, 2000);
    window.onload = function() {
        console.log('页面加载完毕');
    }
     console.log('哈哈哈');
     console.log('嘿嘿嘿');
     console.log('呵呵呵');
</script>

以上例子画图如下:

image-20220209144240573

思考问题:

  1. 把下面例子中的setTimeout 2000改为0, 输出顺序会不会发生改变

    <script>
        setTimeout(function(){
            console.log('时间到了');
        }, 2000); 
         console.log('哈哈哈');
         console.log('嘿嘿嘿');
         console.log('呵呵呵');
    </script>
    

    答: 不会, 因为setTimeout是异步, 必须要等同步任务执行完, 才开始执行异步任务

  2. 若setTimeout的延迟设为1秒, 1秒后setTimeout的回调函数一定会执行吗

    答: 不一定, 什么时候执行取决于有没有同步任务, 以及同步任务执行所需时间

    setTimeout(function () {
      console.log("时间到");
    }, 1000);
    
    var count = 0;
    for(;;){
      count++;
      if (count === 1000000) {
        break;
      }
    };
    
  3. 说出以下代码运行的结果, 并解释为什么

    for (var i = 0; i < 5; i++) {
        setTimeout(function () {
            console.log(i);
        }, 0)
    }
    

    如和修复此bug?

  4. 参考链接: https://www.ruanyifeng.com/blog/2014/10/event-loop.html

(五) this的指向

问题:谈谈this的指向(背诵) 或问:解释下JavaScript中this是如何工作

答: this的指向有以下几种情况

​ 注意: this永远指向函数运行时所在的对象,而不是函数被创建时所在的对象。

  1. 普通的函数调用: 函数被谁调用,this就是谁。
  2. 匿名函数或不处于任何对象中的函数指向window 。
  3. 如果是call, apply, bind,this指向了这三者调用函数时传入的第一个参数
  4. 构造函数中的this, 直接调用函数this的指向同第1点, 使用new操作符, this指向构造函数所创建的实例对象
  5. 箭头函数的指向: 由于箭头函数不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值

(1) 普通的函数调用

函数被谁调用,this就是谁。

 <!DOCTYPE html>
<html lang="en">
 
<body> 

    <script>
       function say() {
           console.log('this:',this);
       } 
       say(); // 实际是: window.say(), 所以this指向window


       var person = {
           name: '张三',
           sayName: function() {
               console.log('this',this);
           }
       }
       person.sayName();  // person调用了sayName,
    </script>
</body> 
</html>

思考

 var person = {
     name: '张三',
     sayName: function() {
         console.log('this',this);
     }
 }

var fn = person.sayName;
fn();  // this指向谁

(2) 匿名函数或不处于任何对象中的函数指向window

var person = {
    name: '张三',
    say: function () {
        setTimeout(function() {
            console.log(this);
        },1000);
    }
}

person.say(); 

(3) call, apply, bind的指向

如果是call, apply, bind,this指向了这三者调用函数时传入的第一个参数, 详看 call, apply, bind下一个知识点: call, apply和bind的区别

(4) 构造函数的this指向

构造函数的话,如果不用new操作符而直接调用,那即this指向window。用new操作符生成对象实例后,this就指向了新生成的对象。

(5) 箭头函数的this

由于箭头函数不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值: 将在es6新特性里细讲

(六) call, apply和bind的区别

问题: call, apply和bind有什么区别

三者都会改变this的指向, 区别:

1. call 和 apply 的的使用

使用call和apply都功能相同, 都能改变this的指向

<!DOCTYPE html>
<html lang="en">

<body> 
    <script>
        // 全局变量, window.username
        var username = '张三';
        var obj1 = {
            username: '李四'
        }
        var obj2 = {
            username: '王五'
        } 

        function say() {
            console.log(this, this.username);
        }
        // 直接调用
        console.log('直接调用');
        say(); // this指向window, this.username => window.useranme => 张三

 
        /**
         * 使用call,让this指向obj1
         * 第一个参数 函数运行时this指向的对象
         * 第二个参数,第三个参数...., 就是函数调用所需的参数(后面解释)
         */
        console.log('通过call调用');
        say.call(obj1);
 
        // 使用apply,让this指向obj2
        console.log('通过apply调用');
        say.apply(obj2); 
    </script>
</body>
</html>

2. call和apply的区别

  • call的第一个参数是this要指向的对象, 第二个,第三个....都是调用原函数所需要的参数。

  • apply第一个参数是this要指向的对象, 第二个参数是数组或类数组, 数组是放的是调用原函数所需要的参数。

<script>
    // 全局变量, window.username
    var username = '张三';
    var obj1 = {
        username: '李四'
    }
    var obj2 = {
        username: '王五'
    } 

    function say(provice,city) { 
        var str = `我叫${this.username}, 我来自${provice} ${city}`;
        console.log(str);
    } 

    // 直接调用
    // say('广东省','深圳市');

    console.log('通过call调用');
    say.call(obj1,'广西省','百色市');

    console.log('通过apply调用');
    say.apply(obj2,['河南省','驻马店']); 
</script>

3. bind 和 call/apply 有一个很重要的区别

  • 一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数, 不会直接调用
  • 当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this, 新函数的调用和原来函数的调用一模一样,除了this的指向不一样之外
<script>
    // 全局变量, window.username
    var username = '张三';
    var obj = {
        username: '李四'
    } 

    function say(provice,city) { 
        var str = `我叫${this.username}, 我来自${provice} ${city}`;
        console.log(str);
    }  

    // bind返回一个新函数
    var newSay = say.bind(obj);
    console.log(typeof newSay);

    // 调用新函数
    newSay('黑龙江省','哈尔滨'); 
</script>

(七) 闭包及其作用

(1) 闭包是什么, 闭包形成的原因(背诵)

**闭包的概念:**闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数(函数中的函数)。

简单概括: 闭包就是一个能访问另外一个函数内部变量的函数

  • 是一个函数
  • 能访问另外一个函数的内部变量

**闭包形成的原因:**函数内部的局部变量一般在函数运行结束的时候就会被销毁, 但如果局部变量因为被外部引用而导致没有被销毁, 就形成了闭包

闭包的特性:

闭包可以让我们在函数外部访问函数内部的变量

<script>
 function foo() {
  var name = "foo";
  var desc = 'foo函数';

  function goo() {
    var name = "goo";
    console.log(desc);
  }
}
</script>

思考问题(1): 如何在函数外部访问函数内部变量

// 1.在foo中返回goo函数
function foo() {
  var name = "foo";
  var desc = 'foo函数';

  function goo() {
    var name = "goo";
    return desc;
  }

  return goo; 
}

// 2.获取goo函数,赋值给fn
var fn = foo(); // fn就是goo
var desc = fn();  // 相当于goo();
// 上面两句也可以合并成一句
// var desc = foo()(); 
console.log('desc',desc);

思考问题(2): 为什么滥用闭包可能会引起内存泄漏

  1. 一般情况下, 函数内部变量会在函数运行结束后被销毁
  2. 使用闭包的时候, 闭包相关的变量不会被回收, 所以滥用闭包可能会引起内存泄漏

使用闭包解决setTimeout问题

一下代码的执行结果是5个5

for (var i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i);
    }, 0)
}

使用闭包, 让以上代码输出想要结果0,1,2,4

for (var i = 0; i < 5; i++) { 
  (function (num) {  
    setTimeout(function () {
      console.log(num);
    }, num*1000);
  })(i);  
}

使用es6解决

for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}

(2) 闭包有哪些作用(背诵)

或问: 闭包有哪些应用场景

  1. 封装私有变量(私有属性)
  2. 模仿块级作用域(ES5中没有块级作用域)
  3. 实现JS的模块
1.封装私有变量(私有属性)

私有变量(私有属性)是后台语言经常用的东西, 意思是这个属性是私有的,不能随意更改,要改,必须通过指定的set和get方法进行获取和修改

function getCat() {
  var _name = "小花";
  // 获取_name
  function get() {
    return _name;
  }

  // 修改名字
  function set(newName) {
    _name = newName;
  }

  return {
    get: get,
    set: set,
  };
}

var cat = getCat();
console.log(cat);

// 或取名字
var name = cat.get();
console.log('name',name);

// 修改
cat.set('小黑');
var name = cat.get();
console.log('name',name);
2.模仿块级作用域

ES5中没有块级作用域, 模仿块级作用域的意思就是让变量只在{}内起作用

// 1.js没有块级作用域
<script>
    // 什么是块级作用域
    {
        var a = 2;
    } 
    console.log(a); 

    for(var i=0;i<5;i++) {

    }
    console.log(i);
</script> 

// 3.使用闭包模拟块级作用: num只在 {}内有效
for (var i = 0; i < 5; i++) { 
  (function (num) {  
    setTimeout(function () {
      console.log(num);
    }, num*1000);
  })(i);  
}
3.实现JS的模块(js库)

js模块, 比如axios.js 就是一个js的模块, axios提供了get,post等方法

(function(window) {
  function get(url,data) {
    // todo
  }

  function post(url,data) {
    // todo
  } 

  window.httpObj = {
    get:get,
    post:post
  }
})(window); 
使用闭包的注意点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题

(八) 深拷贝和浅拷贝

概念和定义

定义: 深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。

**浅拷贝:**仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。

**深拷贝:**在计算机中开辟一块新的内存地址用于存放复制的对象。

TIP

浅拷贝例子

浅拷贝只拷贝了对象的引用(内存地址), 当原对象发生了改变, 新对象也跟着发生改变, 因为它们都是指向了同一个对象

 var money = {
  bank:'平安银行',
  count: 1000000
}

var money2 = money;
money.count = 100;
console.log(money2.count);

TIP

深拷贝: 只拷贝第一层

深拷贝分两种情况

  • 只拷贝第一层
  • 完全拷贝

1. 深拷贝方法 for in 的方法只能拷贝第一层

var money = {
  bank:'平安银行',
  count: 1000000,
} 

// 创建一个新对象
var money2 = {};  
for(var key in money) {
  money2[key] = money[key];
}

money.count = 1000;
console.log(money2.count);

使用for in只能拷贝第一层

var money = {
  bank:'平安银行',
  count: 1000000,
  info: {
    name: '张三',
    age:20
  }
}  
// 创建一个新对象
var money2 = {};  
for(var key in money) {
  if(type of money[key] === 'object')
}
 
money.info.age = 30;
console.log(money2.info.age);  // 修改了money的age,money2的age也发生了改变, 因为只拷贝了第一层

image-20220210145159177

2. 使用es6的扩展运算符得到的对象也是只拷贝第一层

var money = {
  bank:'平安银行',
  count: 1000000,
  info: {
    name: '张三',
    age:20
  }
}  
 
// 得到一个新对象
var money2 = {
  ...money
}
 
money.info.age = 30;
console.log(money2.info.age);

TIP

完全拷贝

3. 使用JSON的提供api

  • JSON.stringify() 将对象转成json字符串
  • JSON.parse() 将json字符串转成对象
 var money = {
  bank: '平安银行',
  count: 1000000,
  info: {
      name: '张三',
      age: 20,
      addr: {
          provice: '省份',
          city: '深圳'
      }
  }
}

// 将对象转成json字符串
var str = JSON.stringify(money);  
var money2 = JSON.parse(str);
// 修改money数据
money.info.name = '李四';
// 新对象money2不会发生改变, 因为money2和money完全没有关系
console.log(money2.info.name);

4. 函数递归调用

for循环深拷贝

<script>
        var money = {
            bank: '平安银行',
            count: 1000000,
            info: {
                name: '张三',
                age: 20
            },
            a: null
        }

        // 得到一个新对象
        var money2 = {};

        for (var key in money) {
            var value = money[key];
            if (typeof value === 'object' && value) {
                money2[key] = {};
                for (var p in value) { 
                    money2[key][p] = money[key][p];
                }
            } else {
                money2[key] = money[key];
            }
        }

        money.info.age = 30;
        console.log(money2.info.age);
    </script>

使用函数递归, 对所有引用数据类型的值进行复制,一直到到没有值是引用数据类型位置

<script>
        var money = {
            bank: '平安银行',
            count: 1000000,
            info: {
                name: '张三',
                age: 20,
                addr: {
                    provice: '省份',
                    city: '深圳',
                    a: {
                        x:22,
                        y:33
                    }
                }
            },
            a: null
        }

        function clone(obj) {
            var newObj = {};
            for (var key in obj) {
                var value = obj[key];
                if (typeof value === 'object' && value) {
                    newObj[key] = clone(value);
                } else {
                    newObj[key] = obj[key];
                }
            }
            return newObj;
        }

        var money2 = clone(money);
        console.log(money2);
        money.info.age = 30;
        console.log(money2.info.age);
    </script>  

其它方法:

https://www.jianshu.com/p/1c142ec2ca45

(九) 严格模式'use strict'(了解)

在代码中出现表达式-"use strict"; 意味着代码按照严格模式解析,这种模式使得Javascript在更严格的条件下运行。

好处:

  1. 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
  2. 消除代码运行的一些不安全之处,保证代码运行的安全;
  3. 提高编译器效率,增加运行速度;
  4. 为未来新版本的Javascript做好铺垫。

坏处:

  1. 同样的代码,在"严格模式"中,可能会有不一样的运行结果;

    普通模式下, this指向window

    严格模式, this的值为undefined

    <script>
        "use strict"
        function say() {
            console.log(this);
        } 
        say();
    </script>
    
  2. 一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。

    • 正常模式下声明变量缺少var会变成全局变量
    • 严格模式下, 不允许这样做, 下面的代码会报错
    "use strict"
    function say() {
    	var a = 100;
    	b = 200;
    }
    
    say();
    console.log(b);
    

(十) 防抖和节流

概念

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如滚动、输入等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数,防抖和节流是比较好的解决方案。

(1)所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。使用场景:注册的时候检查用户名是否已经被注册.

(2)所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。使用场景:监听滚动条是否到了顶部或底部.

(1) 防抖例子

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <input type="text">
    <script>
        var input = document.querySelector('input');
        var timer;
        input.addEventListener('input', function () {
            // 清除延迟器 
            clearTimeout(timer);
            var value = document.querySelector('input').value;
            timer = setTimeout(function () {
                console.log('发送请求到后台')
            }, 300);
        }, false);
    </script>
</body> 
</html>

封装函数

// 上面例子中的函数,一般单独写出来,给它命名为debounce
 <!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <input type="text">
    <script>
        var input = document.querySelector('input');
        input.addEventListener('input', debounce(1000), false);

        function debounce(count) {
            count = count || 300;
            var timer;
            return function () {
                clearTimeout(timer);
                var value = document.querySelector('input').value;
                timer = setTimeout(function () {
                    console.log('发送请求到后台')
                }, count);
            }
        }
    </script>
</body> 
</html>

(2) 节流例子

<!DOCTYPE html>

<head>
    <meta charset="UTF-8">
    <style>
        p {height: 100px}
    </style>
</head>

<body>
    <div id="box">
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
        <p>ppppppppppppppppp</p>
    </div>

    <script>
        window.onscroll = cal;
        // 声明开始时间
        var time = new Date();
        function cal() {
            // time2滚动事件触发的时间
            var time2 = new Date();
            if(time2-time >= 1000) {
                console.log('发请求....');
                // ... 其它的代码
                time = time2;
            } 
        }
    </script>
</body>


// 封装节流函数
window.onscroll = throttle(300); 
function throttle(count) {
    // 声明开始时间
    var time = new Date();
    return function () {
        // time2滚动事件触发的时间
        var time2 = new Date();
        if (time2 - time >= count) {
            console.log('发请求....');
            // ... 其它的代码
            time = time2;
        }
    }
}

(3) 防抖和节流的区别

函数防抖和节流区别在于,当事件持续被触发,如果触发时间间隔短于规定的等待时间(n秒),那么

  • 函数防抖的情况下,函数将一直推迟执行,造成不会被执行的效果;
  • 函数节流的情况下,函数将每个 n 秒执行一次。

(十一) 队列和栈

队列和栈都是后台语言的数据结构, js可以通过数组的一些方法进行模拟队列和栈

  1. 定义: 栈是一种后进先出的数据结构,也就是说最新添加(后进)的项最早被移出;它是一种运算受限的线性表,只能在表头/栈顶进行插入和删除操作。栈有栈底和栈顶。
  2. 入栈和出栈: 向一个栈插入新元素叫入栈(进栈),就是把新元素放入到栈顶的上面,成为新的栈顶;从一个栈删除元素叫出栈,就是把栈顶的元素删除掉,相邻的成为新栈顶;
  3. js数组方法模拟栈的数据结构
    1. 入栈 push()
    2. 出栈 pop()

队列

  1. 定义: 队列是一种先进先出的数据结构。 队列在列表的末端增加项,在首端移除项。
  2. 出队列和入队列:队列允许在表的首端(队列头)进行删除操作,在表的末端(队列尾)进行插入操作;(例如在银行窗口排队办理业务,最前面的第一个人开始办理,后面来的人只能在队伍末尾排队,直到排到他们为止),队列是实现多任务的重要机制!
  3. js数组模拟入队列和出队列
    • 入队列 unshift()
    • 出队列 pop()

(十二) 算法

计算机算法是用计算机解决问题的方法、步骤。解决不同的问题,需要不同的算法。

js数组的sort方法使用的是插入排序算法和快速排序算法

参考链接: https://www.cnblogs.com/AlbertP/p/10847627.html

js十大经典排序算法

img

<script>
    function bubbleSort(arr) {
    console.log(arr);
    var len = arr.length;
    for (var i = 0; i < len; i++) {
        // i循环一次,j循环len - 1 - i次
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) { //相邻元素两两对比
                var temp = arr[j + 1]; //元素交换
                arr[j + 1] = arr[j];
                arr[j] = temp;
            }
        }
        console.log(i,arr);
    }
    return arr;
}


var arr = [8,2,9,1,7,3,6,4,5];
var newArr = bubbleSort(arr); 
</script> 

img

排序演示代码

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <style>
        .box { margin-top: 50px; border-bottom: 1px solid; display: flex; align-items: flex-end; width: 500px;}
        p { width: 40px; text-align: center; display: flex; align-items: center; justify-content: center; }
        p+p {margin-left: 20px;}
        .p1 { background-color: gray; height: 160px; }
        .p2 { background-color: gold; height: 40px; }
        .p3 { background-color: green; height: 180px; }
        .p4 { background-color: blue; height: 20px; }
        .p5 { background-color: pink; height: 140px; }
        .p6 { background-color: salmon; height: 60px; }
        .p7 { background-color: rgb(49, 199, 86); height: 120px; }
        .p8 { background-color: rgb(250, 114, 155); height: 80px; }
        .p9 { background-color: rgb(98, 62, 228); height: 100px; }
    </style>
</head>

<body>
    <div class="box">
        <p class="p1">8</p>
        <p class="p2">2</p>
        <p class="p3">9</p>
        <p class="p4">1</p>
        <p class="p5">7</p>
        <p class="p6">3</p>
        <p class="p7">6</p>
        <p class="p8">4</p>
        <p class="p9">5</p>
    </div>
</body> 
</html>

(十三) Eslint

http://docs.huruqing.cn/第12章 工具/VsCode.html#eslint配置

(1) Eslint常用命令

  1. 忽略 no-undef 检查 /* eslint-disable no-undef */

  2. 忽略 no-new 检查 /* eslint-disable no-new */

  3. 检查且修复 eslint * --fix

  4. 检查指定文件 eslint app.js --fix

  5. eslint初始化

    npm init -y
    npx eslint --init
    

(2) Eslint配置

  1. 安装eslint

    npm i eslint -g
    

    vscode搜索插件eslint安装

  2. .eslintrc.js配置,使用npx eslint --init创建基本配置, 得到以下配置

     module.exports = {
        "env": {
            "browser": true,
            "es2021": true
        },
        "extends": "eslint:recommended",
        "parserOptions": {
            "ecmaVersion": "latest",
            "sourceType": "module"
        },
        "rules": {
        }
    }
    
  3. setting.json配置

    {
        // 其它设置
        .....
        
        // eslint配置
        "eslint.alwaysShowStatus": true, // 显示eslint状态
        "eslint.format.enable": true,  // 使用eslint格式化代码
        "editor.codeActionsOnSave": {
            // 保存时自动修复所有错误
            "source.fixAll.eslint": true
        }
    }
    
  4. 使用eslint规则格式化代码

    • 文件 -> 首选项 -> 扩展 -> eslint

      • eslint > format:Enable 把eslint作为一个格式化工具
      • 右键-> 使用...格式化文档 -> 配置默认格式化程序 -> 选择eslint
      • Eslint: always show status 在任务栏显示eslint状态
    • 保存时自动修复问题

       "editor.codeActionsOnSave": {
          // 保存时自动修复所有错误
          "source.fixAll.eslint": true
        },
      
  5. 自定义规则

     'rules': {
            'no-alert': 0, //禁止使用alert confirm prompt
            'no-array-constructor': 2, //禁止使用数组构造器
            'no-bitwise': 0, //禁止使用按位运算符
            'no-caller': 1, //禁止使用arguments.caller或arguments.callee
            'no-catch-shadow': 2, //禁止catch子句参数与外部作用域变量同名
            'no-class-assign': 2, //禁止给类赋值
            'no-cond-assign': 2, //禁止在条件表达式中使用赋值语句
            'no-console': 0, //禁止使用console
            'no-const-assign': 2, //禁止修改const声明的变量
            'no-constant-condition': 2, //禁止在条件中使用常量表达式 if(true) if(1)
            'no-continue': 0, //禁止使用continue
            'no-control-regex': 2, //禁止在正则表达式中使用控制字符
            'no-debugger': 2, //禁止使用debugger
            'no-delete-var': 2, //不能对var声明的变量使用delete操作符
            'no-div-regex': 1, //不能使用看起来像除法的正则表达式/=foo/
            'no-dupe-keys': 2, //在创建对象字面量时不允许键重复 {a:1,a:1}
            'no-dupe-args': 2, //函数参数不能重复
            'no-duplicate-case': 2, //switch中的case标签不能重复
            'no-else-return': 2, //如果if语句里面有return,后面不能跟else语句
            'no-empty': 2, //块语句中的内容不能为空
            'no-empty-character-class': 2, //正则表达式中的[]内容不能为空
            'no-empty-label': 0, //禁止使用空label
            'no-eq-null': 2, //禁止对null使用==或!=运算符
            'no-eval': 1, //禁止使用eval
            'no-ex-assign': 2, //禁止给catch语句中的异常参数赋值
            'no-extend-native': 2, //禁止扩展native对象
            'no-extra-bind': 2, //禁止不必要的函数绑定
            'no-extra-boolean-cast': 2, //禁止不必要的bool转换
            'no-extra-parens': 2, //禁止非必要的括号
            'no-extra-semi': 2, //禁止多余的冒号
            'no-fallthrough': 1, //禁止switch穿透
            'no-floating-decimal': 2, //禁止省略浮点数中的0 .5 3.
            'no-func-assign': 2, //禁止重复的函数声明
            'no-implicit-coercion': 1, //禁止隐式转换
            'no-implied-eval': 2, //禁止使用隐式eval
            'no-inline-comments': 0, //禁止行内备注
            'no-inner-declarations': [2, 'functions'], //禁止在块语句中使用声明(变量或函数)
            'no-invalid-regexp': 2, //禁止无效的正则表达式
            'no-invalid-this': 2, //禁止无效的this,只能用在构造器,类,对象字面量
            'no-irregular-whitespace': 2, //不能有不规则的空格
            'no-iterator': 2, //禁止使用__iterator__ 属性
            'no-label-var': 2, //label名不能与var声明的变量名相同
            'no-labels': 2, //禁止标签声明
            'no-lone-blocks': 2, //禁止不必要的嵌套块
            'no-lonely-if': 2, //禁止else语句内只有if语句
            'no-loop-func': 1, //禁止在循环中使用函数(如果没有引用外部变量不形成闭包就可以)
            'no-mixed-requires': [0, false], //声明时不能混用声明类型
            'no-mixed-spaces-and-tabs': [2, false], //禁止混用tab和空格
            'linebreak-style': [0, 'windows'], //换行风格
            'no-multi-spaces': 1, //不能用多余的空格
            'no-multi-str': 2, //字符串不能用\换行
            'no-multiple-empty-lines': [1, { 'max': 2 }], //空行最多不能超过2行
            'no-native-reassign': 2, //不能重写native对象
            'no-negated-in-lhs': 2, //in 操作符的左边不能有!
            'no-nested-ternary': 0, //禁止使用嵌套的三目运算
            'no-new': 1, //禁止在使用new构造一个实例后不赋值
            'no-new-func': 1, //禁止使用new Function
            'no-new-object': 2, //禁止使用new Object()
            'no-new-require': 2, //禁止使用new require
            'no-new-wrappers': 2, //禁止使用new创建包装实例,new String new Boolean new Number
            'no-obj-calls': 2, //不能调用内置的全局对象,比如Math() JSON()
            'no-octal': 2, //禁止使用八进制数字
            'no-octal-escape': 2, //禁止使用八进制转义序列
            'no-param-reassign': 2, //禁止给参数重新赋值
            'no-path-concat': 0, //node中不能使用__dirname或__filename做路径拼接
            'no-plusplus': 0, //禁止使用++,--
            'no-process-env': 0, //禁止使用process.env
            'no-process-exit': 0, //禁止使用process.exit()
            'no-proto': 2, //禁止使用__proto__属性
            'no-redeclare': 2, //禁止重复声明变量
            'no-regex-spaces': 2, //禁止在正则表达式字面量中使用多个空格 /foo bar/
            'no-restricted-modules': 0, //如果禁用了指定模块,使用就会报错
            'no-return-assign': 1, //return 语句中不能有赋值表达式
            'no-script-url': 0, //禁止使用javascript:void(0)
            'no-self-compare': 2, //不能比较自身
            'no-sequences': 0, //禁止使用逗号运算符
            'no-shadow': 2, //外部作用域中的变量不能与它所包含的作用域中的变量或参数同名
            'no-shadow-restricted-names': 2, //严格模式中规定的限制标识符不能作为声明时的变量名使用
            'no-spaced-func': 2, //函数调用时 函数名与()之间不能有空格
            'no-sparse-arrays': 2, //禁止稀疏数组, [1,,2]
            'no-sync': 0, //nodejs 禁止同步方法
            'no-ternary': 0, //禁止使用三目运算符
            'no-trailing-spaces': 1, //一行结束后面不要有空格
            'no-this-before-super': 0, //在调用super()之前不能使用this或super
            'no-throw-literal': 2, //禁止抛出字面量错误 throw "error";
            'no-undef': 1, //不能有未定义的变量
            'no-undef-init': 2, //变量初始化时不能直接给它赋值为undefined
            'no-undefined': 2, //不能使用undefined
            'no-unexpected-multiline': 2, //避免多行表达式
            'no-underscore-dangle': 1, //标识符不能以_开头或结尾
            'no-unneeded-ternary': 2, //禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
            'no-unreachable': 2, //不能有无法执行的代码
            'no-unused-expressions': 2, //禁止无用的表达式
            'no-unused-vars': [2, { 'vars': 'all', 'args': 'after-used' }], //不能有声明后未被使用的变量或参数
            'no-use-before-define': 2, //未定义前不能使用
            'no-useless-call': 2, //禁止不必要的call和apply
            'no-void': 2, //禁用void操作符
            'no-var': 0, //禁用var,用let和const代替
            'no-warning-comments': [
                1,
                { 'terms': ['todo', 'fixme', 'xxx'], 'location': 'start' }
            ], //不能有警告备注
            'no-with': 2, //禁用with
    
            'array-bracket-spacing': [2, 'never'], //是否允许非空数组里面有多余的空格
            'arrow-parens': 0, //箭头函数用小括号括起来
            'arrow-spacing': 0, //=>的前/后括号
            'accessor-pairs': 0, //在对象中使用getter/setter
            'block-scoped-var': 0, //块语句中使用var
            'brace-style': [1, '1tbs'], //大括号风格
            'callback-return': 1, //避免多次调用回调什么的
            'camelcase': 2, //强制驼峰法命名
            'comma-dangle': [2, 'never'], //对象字面量项尾不能有逗号
            'comma-spacing': 0, //逗号前后的空格
            'comma-style': [2, 'last'], //逗号风格,换行时在行首还是行尾
            'complexity': [0, 11], //循环复杂度
            'computed-property-spacing': [0, 'never'], //是否允许计算后的键名什么的
            'consistent-return': 0, //return 后面是否允许省略
            'consistent-this': [2, 'that'], //this别名
            'constructor-super': 0, //非派生类不能调用super,派生类必须调用super
            'curly': [2, 'all'], //必须使用 if(){} 中的{}
            'default-case': 2, //switch语句最后必须有default
            'dot-location': 0, //对象访问符的位置,换行的时候在行首还是行尾
            'dot-notation': [0, { 'allowKeywords': true }], //避免不必要的方括号
            'eol-last': 0, //文件以单一的换行符结束
            'eqeqeq': 2, //必须使用全等
            'func-names': 0, //函数表达式必须有名字
            'func-style': [0, 'declaration'], //函数风格,规定只能使用函数声明/函数表达式
            'generator-star-spacing': 0, //生成器函数*的前后空格
            'guard-for-in': 0, //for in循环要用if语句过滤
            'handle-callback-err': 0, //nodejs 处理错误
            'id-length': 0, //变量名长度
            'indent': [2, 4], //缩进风格
            'init-declarations': 0, //声明时必须赋初值
            'key-spacing': [0, { 'beforeColon': false, 'afterColon': true }], //对象字面量中冒号的前后空格
            'lines-around-comment': 0, //行前/行后备注
            'max-depth': [0, 4], //嵌套块深度
            'max-len': [0, 80, 4], //字符串最大长度
            'max-nested-callbacks': [0, 2], //回调嵌套深度
            'max-params': [0, 3], //函数最多只能有3个参数
            'max-statements': [0, 10], //函数内最多有几个声明
            'new-cap': 2, //函数名首行大写必须使用new方式调用,首行小写必须用不带new方式调用
            'new-parens': 2, //new时必须加小括号
            'newline-after-var': 2, //变量声明后是否需要空一行
            'object-curly-spacing': [0, 'never'], //大括号内是否允许不必要的空格
            'object-shorthand': 0, //强制对象字面量缩写语法
            'one-var': 1, //连续声明
            'operator-assignment': [0, 'always'], //赋值运算符 += -=什么的
            'operator-linebreak': [2, 'after'], //换行时运算符在行尾还是行首
            'padded-blocks': 0, //块语句内行首行尾是否要空行
            'prefer-const': 0, //首选const
            'prefer-spread': 0, //首选展开运算
            'prefer-reflect': 0, //首选Reflect的方法
            'quotes': [1, 'single'], //引号类型 `` "" ''
            'quote-props': [2, 'always'], //对象字面量中的属性名是否强制双引号
            'radix': 2, //parseInt必须指定第二个参数
            'id-match': 0, //命名检测
            'require-yield': 0, //生成器函数必须有yield
            'semi': [2, 'always'], //语句强制分号结尾
            'semi-spacing': [0, { 'before': false, 'after': true }], //分号前后空格
            'sort-vars': 0, //变量声明时排序
            'space-after-keywords': [0, 'always'], //关键字后面是否要空一格
            'space-before-blocks': [0, 'always'], //不以新行开始的块{前面要不要有空格
            'space-before-function-paren': [0, 'always'], //函数定义时括号前面要不要有空格
            'space-in-parens': [0, 'never'], //小括号里面要不要有空格
            'space-infix-ops': 0, //中缀操作符周围要不要有空格
            'space-return-throw-case': 0, //return throw case后面要不要加空格
            'space-unary-ops': [0, { 'words': true, 'nonwords': false }], //一元运算符的前/后要不要加空格
            'spaced-comment': 0, //注释风格要不要有空格什么的
            'strict': 2, //使用严格模式
            'use-isnan': 2, //禁止比较时使用NaN,只能用isNaN()
            'valid-jsdoc': 0, //jsdoc规则
            'valid-typeof': 2, //必须使用合法的typeof的值
            'vars-on-top': 2, //var必须放在作用域顶部
            'wrap-iife': [2, 'inside'], //立即执行函数表达式的小括号风格
            'wrap-regex': 0, //正则表达式字面量用小括号包起来
            'yoda': [2, 'never'] //禁止尤达条件
        }
    

作业

  1. 完成每日生鲜搜索功能 (使用防抖功能)

    接口地址: http://huruqing.cn:3003/product/search

    <script>
        var $inp = document.querySelector('#inp');
        $inp.oninput = debounce();
    
        function debounce() {
          var timer;
          return function () {
            clearTimeout(timer);
            timer = setTimeout(function () {
              // 获取搜索关键词
              var keyword = $inp.value; 
              getData(keyword);
            }, 1000);
          }
        }
    
        // 发请求函数
        function getData(keyword) {
          var xhr = new XMLHttpRequest();
          var url = 'http://huruqing.cn:3003/product/search?keyword=' + keyword;
          xhr.open('GET', url);
          xhr.send();
          xhr.onreadystatechange = function () {
            if (xhr.status === 200 && xhr.readyState === 4) {
              var res = JSON.parse(xhr.responseText);
              // 渲染列表
              render(res.list);
            }
          }
        }
    
        // 渲染页面
        function render(list) {
          var htmlStr = '';
          list.forEach(function (item) {
            htmlStr += `<div class="item bd">
            <img src="${item.imgUrl}"
              alt="" class="img" />
            <p class="t-box bd">${item.masterName}</p>
          </div> `;
          })  
          document.getElementById('godd-list').innerHTML = htmlStr;
        }
      </script>
    
  2. 说出以下输出结果, 并解释为什么

    <script>
        function fun() {
            var r = [];
            var i = 0;
            for (; i < 3; i++) {
                r[i] = function () {
                    return i;
                }
            }
            return r;
        }
    
        var fun2 = fun();
        // fun2 [fn,fn,fn] 
    
        for (var i = 0; i < fun2.length; i++) {
            // fun2[i] 是一个函数, fun2[i]() 调用函数
            console.log(fun2[i]());
        }
    </script>
    
  3. 说出以下代码运行结果, 并解释为什么

    var name = '小明';
    var obj = {
      name: '小红',
      getName: function() {
        return function() {
          return this.name;
        }
      }
    } 
    console.log(obj.getName()());